JS-实现继承的五种办法

前言

之前一直对原型链不太了解,所以继承也懵懵懂懂,最近终于对原型链有点心得了,抓紧弄懂继承!

原型链继承

核心:将父类的实例作为子类的原型

特点:

  • 实例 是子类的 实例,也是父类的 实例
  • 父类新增原型方法/原型属性,子类都能访问到

缺点:

  • 父类一变,子类也变
  • 无法实现多继承:一个子类只能继承一个父类,不能继承多个父类
  • 子类新增属性/方法,必须在new () 后面执行
  • 创建子类实例时,无法向父类构造函数传参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function Parent(name) {
this.name = 'wei' || name;
this.friends = ['小李','小红'];
}
Parent.prototype.getName = function() { //对原型进行扩展
return this.name;
}

function Child(age) {
this.age = age;
}
Child.prototype = new Parent(); //关键!!!通过构造器函数创建一个新对象,把老对象的东西拿过来
Child.prototype.constructor = Child; //这一步也很关键
// 所有涉及到原型链继承的继承方法都要修改子类构造函数的指向,否则子类实例的构造函数指向父类

//注意,不要在父类构造函数传参,虽然语法上没啥问题,但是实际上并不符合 面向对象 编程规则:对象(实例)才是属性的拥有者
//如果创建子类实例时,传参,给属性赋值,就变成类拥有属性,而不是对象拥有属性
//例子: Parent.prototype = new Person('老明');
//此时,Parent类拥有了 name = '老明'属性,此后,它的实例对象 c1,c2,c3 都只能接受 name 这个属性

Child.prototype.getAge = function() {
return this.age;
}
Child.prototype.getName = function() { // 可以重写父类继承来的方法,会覆盖
console.log('222') ;
}

var result = new Child(22);
console.log(result.getName()); // 222
console.log(result.getAge()); //22
result.friends.push('小微')  // ["小李", "小红", "小微"]
console.log(result.friends);

var result1 = new Child(18);
console.log(result1.friends);  // ["小李", "小红", "小微"]
console.log(result.getName()); // 222

构造继承

核心:在子类构造函数中利用 call/apply 把父类中通过 this 指定的属性/方法 复制到子类创建的实例中

特点:

  • 实例不是父类的实例,只是子类的实例
  • 创建子类实例时,可以向父类传递参数
  • 可以实现多继承(call 多个父类对象)

缺点:

  • 只能继承父类的实例方法/属性,不能继承父类原型上的方法/属性
  • 每个子类都有父类实例对象的副本,影响性能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function Parent (name) {
this.name = name;
this.friends = ['小李','小红'];
this.getName = function () {
return this.name;
}
};
Parent.prototype.geSex = function () { //对原型进行扩展的方法无法复用
console.log("男");
};

function Child (age) {
Parent.call(this,'老明');  //这一句是核心关键
// 这样就会在新 Child 对象上执行 Parent 构造函数中定义的所有对象初始化代码,
// 结果 Child 的每个实例都会具有自己的 friends 属性的副本
this.age = age;
};

var result = new Child(23);
console.log(result.name);    // 老明
result.friends.push('小微')
console.log(result.friends);  // ["小李", "小红", "小微"]
console.log(result.getName());  // 老明
console.log(result.age);    // 23
// console.log(result.getSex());  //这个会报错,调用不到父原型上面扩展的方法

var result1 = new Child(18);
console.log(result1.friends); //["小李", "小红"]

组合继承(最常用)

核心:调用父类构造,继承父类的属性,保留传参的优点;将父类实例作为子类原型,实现父类原型 方法/属性 复用

特点:

  • 结合了 原型链继承 与 构造继承 的优点

缺点:

  • 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function Parent (name) {
this.name = name;
this.friends = ['小李','小红'];
};
Parent.prototype.getName = function () {
return this.name;
};

function Child (age) {
Parent.call(this,'老明');  //这一步很关键
this.age = age;
};
Child.prototype = new Parent('老明');  //这一步也很关键
Child.prototype.constructor = Child; //这一步也很关键
Child.prototype.getName = function() { // 可以重写父类继承来的方法,会覆盖
console.log('222') ;
}

var result = new Child(24);
console.log(result.name);    // 老明
result.friends.push("小智");  
console.log(result.friends);  // ['小李','小红','小智']
console.log(result.getName());  // 222
console.log(result.age);    // 24

var result1 = new Child(25); // 通过借用构造函数都有自己的属性,通过原型享用公共的方法
console.log(result1.name);  // 老明
console.log(result1.friends);  // ['小李','小红']
console.log(result1.getName());  // 222

原型式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Parent =  {
name:'zw',
friends: ['小李','小红'],
getName:function () {
return this.name;
}
}

const child1 = Object.create(Parent)
console.log(child1.name); // 老明
child1.friends.push('小微')
console.log(child1.friends); // ['小李', '小红', '小微']
console.log(child1.getName()); // zw

const child2 = Object.create(Parent);
console.log(child2.friends);  // ['小李', '小红', '小微']

寄生式继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Parent =  {
name:'zw',
friends: ['小李','小红'],
getName:function () {
return this.name;
}
}

const child1 = createChild(Parent)
function createChild(obj){
let o = Object.create(obj); // 通过原型式继承创建新的对象
o.sayHi = function (msg) { // 增强这个对象
console.log(msg);
};
return o; //返回这个对象
}
console.log(child1.name)
child1.friends.push('小微')
console.log(child1.friends); // ['小李', '小红', '小微']
console.log(child1.sayHi('child1')); // child1

const child2 = createChild(Parent);
console.log(child2.friends);  // ['小李', '小红', '小微']
console.log(child1.sayHi('child2')); // child2

寄生组合继承

核心:通过寄生方式,砍掉父类的实例属性。那么在调用两次父类的构造时,就不会初始化两次实例方法/熟悉,避免组合继承的缺点

优点:

  • 非常完美

缺点:

  • 实现较复杂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function Parent(name) {
this.name = name;
this.friends = ['小李','小红'];
}
Parent.prototype.getName = function () {
return this.name;
};

function Child(age) {
// 构造函数继承:将父类构造函数的内容复制给子类的构造函数
Parent.call(this, "老明");
this.age = age;
}

// 核心:因为是对父类原型的复制,所以不包含父类的构造函数;也就不会调用两次父类构造函数造成浪费
inheritPrototype(Child, Parent)
function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype) // 创建了对父类实例的浅复制
prototype.constructor = child // 修正原型的构造函数
child.prototype = prototype // 将子类的原型替换
}

var result = new Child(23);
Child.getName = function() { // 可以重写父类继承来的方法,会覆盖
console.log('222') ;
}
console.log(result.name); // 老明
result.friends.push('小微')
console.log(result.friends); // ['小李', '小红', '小微']
console.log(result.getName()); // 222
console.log(result.age); // 23

var result1 = new Child(25); // 通过借用构造函数都有自己的属性,通过原型享用公共的方法
console.log(result1.friends);  // ['小李','小红']
console.log(result1.getName());  // 222

ES6 - class中的super()实现

核心:调用super方法:创造父类的实例对象this,然后再用子类的构造函数修改this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal { 
constructor() {   
this.type = 'animal';
}
says(say) {   
console.log(this.type + ' says ' + say);
}
}

let animal = new Animal();
animal.says('hello'); //animal says hello

class Cat extends Animal {   
constructor() {     
super();     
this.type = 'cat';   
}
}
let cat = new Cat();
cat.says('hello'); //cat says hello